feat: add enum declarations and multi-arm enum match expressions#336
feat: add enum declarations and multi-arm enum match expressions#336stringhandler wants to merge 1 commit into
Conversation
|
3666ffb needs rebase |
ce66af6 to
0eb01e6
Compare
|
0eb01e6 needs rebase |
0eb01e6 to
2fa2122
Compare
e9d43cd to
ad3760b
Compare
|
Since we added unstable feature support, I think we need to add the |
| /// An enum declaration. | ||
| #[derive(Clone, Debug)] | ||
| pub struct EnumDeclaration { | ||
| file_id: usize, |
There was a problem hiding this comment.
Currently, file_id is accessible via the Span struct, so we can delete it here. Try to do something similar to the other structs inside the Item struct
| pub fn insert_enum( | ||
| &mut self, | ||
| name: AliasName, | ||
| visibility: Visibility, | ||
| variants: Arc<[ResolvedEnumVariant]>, | ||
| ) -> Result<(), Error> { | ||
| if self.current_module().enums.contains_key(&name) | ||
| || self.current_module().aliases.contains_key(&name) | ||
| { | ||
| return Err(Error::RedefinedAlias { name }); | ||
| } | ||
| // An enum is also a `u8` type alias, so its name resolves as a type. | ||
| let resolved = self.resolve(&AliasedType::from(UIntType::U8))?; | ||
| self.current_module_mut() | ||
| .aliases | ||
| .insert(name.clone(), (resolved, visibility)); | ||
| self.current_module_mut() | ||
| .enums | ||
| .insert(name, EnumBinding::new(variants)); | ||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
Throughout the PR there's a lot of code where statements are packed together with no blank lines between logically distinct steps, which makes the individual blocks hard to scan.
Could we add blank lines to separate the logical sections? A couple of simple conventions go a long way. For example, a blank line before a return/Ok(()) that closes a block, and a blank line before a comment that introduces a new step.
| pub fn insert_enum( | |
| &mut self, | |
| name: AliasName, | |
| visibility: Visibility, | |
| variants: Arc<[ResolvedEnumVariant]>, | |
| ) -> Result<(), Error> { | |
| if self.current_module().enums.contains_key(&name) | |
| || self.current_module().aliases.contains_key(&name) | |
| { | |
| return Err(Error::RedefinedAlias { name }); | |
| } | |
| // An enum is also a `u8` type alias, so its name resolves as a type. | |
| let resolved = self.resolve(&AliasedType::from(UIntType::U8))?; | |
| self.current_module_mut() | |
| .aliases | |
| .insert(name.clone(), (resolved, visibility)); | |
| self.current_module_mut() | |
| .enums | |
| .insert(name, EnumBinding::new(variants)); | |
| Ok(()) | |
| } | |
| pub fn insert_enum( | |
| &mut self, | |
| name: AliasName, | |
| visibility: Visibility, | |
| variants: Arc<[ResolvedEnumVariant]>, | |
| ) -> Result<(), Error> { | |
| if self.current_module().enums.contains_key(&name) | |
| || self.current_module().aliases.contains_key(&name) | |
| { | |
| return Err(Error::RedefinedAlias { name }); | |
| } | |
| // An enum is also a `u8` type alias, so its name resolves as a type. | |
| let resolved = self.resolve(&AliasedType::from(UIntType::U8))?; | |
| self.current_module_mut() | |
| .aliases | |
| .insert(name.clone(), (resolved, visibility)); | |
| self.current_module_mut() | |
| .enums | |
| .insert(name, EnumBinding::new(variants)); | |
| Ok(()) | |
| } |
| parse::Item::EnumDeclaration(decl) => { | ||
| let n = decl.variants().len(); | ||
| if n < 2 { | ||
| return Err(Error::Grammar { | ||
| msg: format!("enum '{}' must have at least 2 variants", decl.name()), | ||
| }) | ||
| .with_span(decl); | ||
| } | ||
| let mut sorted: Vec<&parse::EnumVariant> = decl.variants().iter().collect(); | ||
| sorted.sort_by_key(|v| v.discriminant()); | ||
| for w in sorted.windows(2) { |
There was a problem hiding this comment.
Same as above; blank lines between logical steps
There was a problem hiding this comment.
I added some but this is very subjective, so not sure what you had in mind
| parse::SingleExpressionInner::EnumMatch(enum_match) => { | ||
| let arms = enum_match.arms(); | ||
| let span = *enum_match.span(); | ||
| if arms.is_empty() { | ||
| return Err(Error::Grammar { | ||
| msg: "enum match has no arms".to_string(), | ||
| }) | ||
| .with_span(span); | ||
| } | ||
| let enum_name = match arms[0].pattern() { | ||
| MatchPattern::EnumVariant(name, _) => name.clone(), |
There was a problem hiding this comment.
Blank lines between logical steps
There was a problem hiding this comment.
I added some blank lines but I don't know what you had in mind
| } | ||
| let enum_name = match arms[0].pattern() { | ||
| MatchPattern::EnumVariant(name, _) => name.clone(), | ||
| _ => unreachable!("EnumMatch arms have EnumVariant patterns"), |
There was a problem hiding this comment.
unreachable! will panic the whole process if it's ever hit, and this runs on caller-supplied witnesses/env. The mismatch may be impossible today, but a change in pruned representation could reach it. The function already returns Result, so let's return an Err here instead and fail gracefully
There was a problem hiding this comment.
This unreachable and the one below it seem reasonable to me. I don't like the idea of returning an Error here because that tells me we expect that kind of thing to happen, which it is very unlikely given that we construct the tree.
I considered other patterns but all of them are more complicated than this and I don't see the benefit
| let mut arm_map: HashMap<&Identifier, &parse::Expression> = HashMap::new(); | ||
| for arm in arms { | ||
| let MatchPattern::EnumVariant(arm_enum_name, variant) = arm.pattern() else { | ||
| unreachable!("EnumMatch arms have EnumVariant patterns") |
There was a problem hiding this comment.
Same comment regarding the unreachable! block
| (Inner::Disconnect(cc, _), Inner::Disconnect(cp, _)) => { | ||
| check_surviving_witnesses(cc, cp, zero_filled) | ||
| } | ||
| _ => unreachable!("unexpected structural mismatch between commit and pruned trees"), |
There was a problem hiding this comment.
Another unreachable block
There was a problem hiding this comment.
I have replaced this with an Err
ad3760b to
0528aab
Compare
Add enum declarations and enum match expressions to SimplicityHL. Enums are registered as u8 type aliases; each variant has a u8 discriminant Enum match expressions desugar to binary bool-match chains that dispatch on discriminant equality, ensuring invalid discriminants fail via panic rather than silently executing any arm.
0528aab to
cfa1116
Compare
|
Added to unstable features |
Introduces
enumwith explicit u8 discriminants and N-armmatchover enum variants, desugared intojet::eq_8comparison chains at the AST level. Missing witness values are zero-filled before Simplicity witness population; a post-prune check errors if any zero-filled witness appears on a surviving (non-pruned) branch.Example
Implementation notes
Some things to note that are missing:
1. Enums with structured fields.
I originally wanted to implement enums as something like
or potentially even with tuples, like
Inherit(<inner tuple>). This would certainly look cleaner in the witness file (i.e. asACTION: { value = "Inherit("0x....")}, however the enum declaration is not present in scope when parsing the file and this makes decoding it difficult.As a trade off enums must have a u8 encoding, so that they can be parsed from the witness. This explicit declaration should also help when the transaction is examined on the explorer.
Structured enums can possibly be implemented in a future PR.
2. Empty witness filling
By splitting the action/path witness variable into it's own
u8, we now have to specify individual witness variables for each of the actions/paths taken in the program. This means that we would now have to specify values for all witnesses, even if they are not called.I did a deep dive and confirmed that unused witness values are pruned, but calling a program with a witness file or programmatically is still inconvenient. To combat this, I've added a step to fill unspecified witness values with defaults of zero, and then an additional step to make sure that none of the filled values remain when pruned. This is done through
satisfy_with_env, so lib callers should not have to do any extra lifting.3. Enum values in code are out of scope
A limit of the implementation is that using enums in code is not supported. This should be done in another PR.
For example, the following is not supported:
This is unfortunate but the PR is already quite large and tricky to review.
4. Desugaring into
jet::eq_u8chainsUnder the hood, the
matchstatement is desugared into a list ofifs(actuallymatchif you are specific), each with a jet::eq_u8.While it might have been more efficient to utilize an
Either::Leftstructure, hoping for a tree like evaluation, it was difficult to decode a u8 reliably into Left/Right nodes and allow for skipping enum values (e..genum ACTION { One = 1, /* Two = 2 --deprecated */, Three = 3 }). That said, it is not impossible, and I can (reluctantly) create another PR for that implementation if desired.